package permit

import (
	"go-main/library/ecode"
	"go-main/library/log"
	bm "go-main/library/net/http/dam"
	"go-main/library/net/http/dam/middleware/permit/api"
	xmetadata "go-main/library/net/metadata"
	"go-main/library/net/rpc/warden"
	"net/url"
)

const (
	_verifyURI             = "/api/session/verify"
	_permissionURI         = "/x/admin/manager/permission"
	_sessIDKey             = "_AJSESSIONID"
	_sessUIDKey            = "uid"      // manager user_id
	_sessUnKey             = "username" // LDAP username
	_defaultDomain         = ".bilibili.co"
	_defaultCookieName     = "mng-go"
	_defaultCookieLifeTime = 2592000
	CtxRoleIDsKey          = "roleIds"
	CtxGroupIDsKey         = "mgr-group-ids"
	CtxPermissionsKey      = "mgr-permissions"
	CtxPermissionKey       = "mgr-permission"
	CtxPermitWorkflowIDKey = "mgr-permit-workflow-id"
	CtxMGRUsernameKey      = "mgr-username"
)

var (
	ignoreHeaderKeys = map[string]int{
		"connection":    1,
		"postman-token": 1,
		"cookie":        1,
	}
)

// permissions .
type permissions struct {
	UID   int64    `json:"uid"`
	Perms []string `json:"perms"`
}

// Permit is an auth middleware.
type Permit struct {
	verifyURI       string
	permissionURI   string
	dashboardCaller string
	dsClient        *bm.Client // dashboard client
	maClient        *bm.Client // manager-admin client

	sm *SessionManager // user Session

	api.PermitClient // mng grpc client
}

// Verify only export Verify function because of less configure
type Verify interface {
	Verify() bm.HandlerFunc
}

// Config identify config.
type Config struct {
	DsHTTPClient    *bm.ClientConfig // dashboard client config. appkey can not reuse.
	MaHTTPClient    *bm.ClientConfig // manager-admin client config
	Session         *SessionConfig
	ManagerHost     string
	DashboardHost   string
	DashboardCaller string
}

// Config2 .
type Config2 struct {
	MngClient *warden.ClientConfig
	Session   *SessionConfig
}

// New new an auth service.
func New(c *Config) *Permit {
	a := &Permit{
		dashboardCaller: c.DashboardCaller,
		verifyURI:       c.DashboardHost + _verifyURI,
		permissionURI:   c.ManagerHost + _permissionURI,
		dsClient:        bm.NewClient(c.DsHTTPClient),
		maClient:        bm.NewClient(c.MaHTTPClient),
		sm:              newSessionManager(c.Session),
	}
	return a
}

// New2 .
func New2(c *warden.ClientConfig) *Permit {
	permitClient, err := api.NewClient(c)
	if err != nil {
		//panic(errors.WithMessage(err, "Failed to dial mng rpc server"))
	}
	return &Permit{
		PermitClient: permitClient,
		sm:           &SessionManager{},
	}
}

// Verify2 check whether the user has logged in.
func (p *Permit) Verify2() bm.HandlerFunc {
	return func(ctx *bm.Context) {
		sid, _, _, err := p.login2(ctx)
		if err != nil {
			ctx.JSON(nil, ecode.Unauthorized)
			ctx.Abort()
			return
		}
		p.sm.setHTTPCookie(ctx, _defaultCookieName, sid)
	}
}

// Verify return bm HandlerFunc which check whether the user has logged in.
func (p *Permit) Verify() bm.HandlerFunc {
	return func(ctx *bm.Context) {
		si, err := p.login(ctx)
		if err != nil {
			ctx.JSON(nil, ecode.Unauthorized)
			ctx.Abort()
			return
		}
		p.sm.SessionRelease(ctx, si)
	}
}

// Permit return bm HandlerFunc which check whether the user has logged in and has the access permission of the location.
// If `permit` is empty,it will allow any logged in request.
func (p *Permit) Permit(permit string) bm.HandlerFunc {
	return func(ctx *bm.Context) {
		var (
			si    *Session
			uid   int64
			perms []string
			err   error
		)
		si, err = p.login(ctx)
		if err != nil {
			ctx.JSON(nil, ecode.Unauthorized)
			ctx.Abort()
			return
		}
		defer p.sm.SessionRelease(ctx, si)
		uid, perms, err = p.permissions(ctx, si.Get(_sessUnKey).(string))
		if err == nil {
			si.Set(_sessUIDKey, uid)
			ctx.Set(_sessUIDKey, uid)
		}
		if len(perms) > 0 {
			ctx.Set(CtxPermissionsKey, perms)
		}
		if !p.permit(permit, perms) {
			ctx.JSON(nil, ecode.AccessDenied)
			ctx.Abort()
			return
		}
	}
}

// login check whether the user has logged in.
func (p *Permit) login(ctx *bm.Context) (si *Session, err error) {
	si = p.sm.SessionStart(ctx)
	if si.Get(_sessUnKey) == nil {
		var username string
		if username, err = p.verify(ctx); err != nil {
			return
		}
		si.Set(_sessUnKey, username)
	}
	ctx.Set(_sessUnKey, si.Get(_sessUnKey))
	if md, ok := xmetadata.FromContext(ctx); ok {
		md[xmetadata.Username] = si.Get(_sessUnKey)
	}
	return
}

// Permit2 same function as permit function but reply on grpc.
/*func (p *Permit) Permit2(permit string) bm.HandlerFunc {
	return p.PermitZ(permit, make([]*api.PermitCondition, 0), func(ctx *bm.Context) bool {
		return true
	})
}*/

// PermitZ same function as Permit2 function, but be able to add extra query.
/*func (p *Permit) PermitZ(permit string, extraLimits []*api.PermitCondition, needPermit func(ctx *bm.Context) bool) bm.HandlerFunc {
	return func(ctx *bm.Context) {
		// 根据context内容，调用方自行判断是否需要permit
		// 可以据此进行不同业务场景的独立鉴权
		if needPermit != nil && !needPermit(ctx) {
			return
		}

		// 若经过WithPermitContext处理，则仅取permissions值做校验，不做额外逻辑
		// WithPermitContext已包含登陆校验
		if rawPermissions, exists := ctx.Get(CtxPermissionsKey); exists {
			if permit == "" {
				// 若不传要校验的权限点，直接跳过处理
				return
			} else {
				ctx.Set(CtxPermissionKey, permit)
				if permissions, ok := rawPermissions.([]string); ok {
					if slices.Exists(permissions, permit) {
						return
					} else {
						ctx.JSON(nil, ecode.AccessDenied)
						ctx.Abort()
						return
					}
				}
			}
			// 若context中permissions不合法，则用继续用接口校验
		}
		// 若不传要校验的权限点，则只做登陆校验
		if permit == "" {
			_, username, _, err := p.login2(ctx)
			if err != nil || username == "" {
				ctx.JSON(nil, ecode.Unauthorized)
				ctx.Abort()
			}
			return
		}
		permitReq := &api.PermitReq{Permission: permit, ExtraLimits: extraLimits}
		host := ctx.Request.Host
		if matched, err := regexp.MatchString("\\d\\.\\d\\.\\d\\.\\d", host); err != nil || matched {
			switch os.Getenv("DEPLOY_ENV") {
			case "uat":
				host = "uat-manager.bilibili.co"
				break
			case "pre":
				host = "manager-pre.bilibili.co"
				break
			case "prod":
				host = "manager.bilibili.co"
				break
			default:
				log.Warn("go-main.bm.middleware.permit.getHost Error, will not process workflow")
			}
		}
		protocol := api.PermitWorkflowCallbackProtocol_HTTP
		urlSchema := strings.ToUpper(ctx.Request.URL.Scheme)
		if _, exists := api.PermitWorkflowCallbackProtocol_Enum_value[urlSchema]; exists {
			protocol = api.PermitWorkflowCallbackProtocol_Enum(api.PermitWorkflowCallbackProtocol_Enum_value[urlSchema])
		}
		permitReq.RequestInfo = &api.PermitWorkflowRequestInfo{
			Protocol: protocol,
			Url:      host + ctx.Request.RequestURI,
			Method:   ctx.Request.Method,
			Headers:  make(map[string]string),
		}
		if len(ctx.Request.Header) > 0 {
			for headerKey := range ctx.Request.Header {
				if _, exists := ignoreHeaderKeys[strings.ToLower(headerKey)]; !exists {
					permitReq.RequestInfo.Headers[headerKey] = ctx.Request.Header.Get(headerKey)
				}
			}
			if username := ctx.Request.Header.Get(CtxMGRUsernameKey); username != "" {
				permitReq.Username = username
			}
		}
		// 若为workflow回调
		if permitWorkflowID := ctx.Request.Header.Get(CtxPermitWorkflowIDKey); permitWorkflowID != "" {
			if _, err := strconv.ParseInt(permitWorkflowID, 10, 64); err != nil {
				ctx.JSON(nil, ecode.RequestErr)
				ctx.Abort()
				return
			}
			ctx.Context = metadata.AppendToOutgoingContext(ctx.Context, CtxPermitWorkflowIDKey, ctx.Request.Header.Get(CtxPermitWorkflowIDKey))
		} else {
			sid, username, _, err := p.login2(ctx)
			if err != nil {
				ctx.JSON(nil, ecode.Unauthorized)
				ctx.Abort()
				return
			}
			p.sm.setHTTPCookie(ctx, _defaultCookieName, sid)
			var requestBodyBytes []byte
			if ctx.Request.Body != nil {
				requestBodyBytes, _ = ioutil.ReadAll(ctx.Request.Body)
				ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(requestBodyBytes))
			}
			permitReq.RequestInfo.Body = string(requestBodyBytes)
			permitReq.Username = username
		}

		reply, err := p.PermitClient.Permit(ctx.Context, permitReq)
		if err != nil {
			if ecode.EqualError(ecode.New(77801), err) {
				// 如果是生成了工单，不直接拦截。业务方自行处理
				ctx.Error = err
			} else {
				log.Errorf("go-main.bm.middleware.permit.Permit(%v) Error (%+v)", permitReq, err)
				ctx.JSON(nil, ecode.AccessDenied)
				ctx.Abort()
				return
			}
		}
		if reply == nil {
			log.Errorf("go-common.bm.middleware.permit.Permit(%v) Error empty reply. Please contact MGR admin.", permitReq, err)
			ctx.JSON(nil, ecode.ServerErr)
			ctx.Abort()
			return
		}
		if !reply.CanAccess {
			ctx.JSON(nil, ecode.AccessDenied)
			ctx.Abort()
			return
		} else if reply.PermitWorkflowID > 0 {
			ctx.Error = errors.WithMessage(ecode.New(77801), strconv.FormatInt(reply.PermitWorkflowID, 10))
		}
	}
}*/

// login2 .
func (p *Permit) login2(ctx *bm.Context) (sid, uname string, uid int64, err error) {
	var dsbsid, mngsid string
	dsbck, err := ctx.Request.Cookie(_sessIDKey)
	if err == nil {
		dsbsid = dsbck.Value
	}
	if dsbsid == "" {
		err = ecode.Unauthorized
		return
	}
	mngck, err := ctx.Request.Cookie(_defaultCookieName)
	if err == nil {
		mngsid = mngck.Value
	}
	reply, err := p.Login(ctx, &api.LoginReq{Mngsid: mngsid, Dsbsid: dsbsid})
	if err != nil {
		log.Errorf("mng rpc Login error(%v)", err)
		return
	}
	sid = reply.Sid
	uname = reply.Username
	uid = reply.Uid
	ctx.Set(_sessUnKey, uname)
	ctx.Set(_sessIDKey, sid)
	if _, exists := ctx.Get(_sessUIDKey); !exists {
		ctx.Set(_sessUIDKey, uid)
	}
	return
}

func (p *Permit) verify(ctx *bm.Context) (username string, err error) {
	var (
		sid string
		r   = ctx.Request
	)
	session, err := r.Cookie(_sessIDKey)
	if err == nil {
		sid = session.Value
	}
	if sid == "" {
		err = ecode.Unauthorized
		return
	}
	username, err = p.verifyDashboard(ctx, sid)
	return
}

// permit check whether user has the access permission of the location.
func (p *Permit) permit(permit string, permissions []string) bool {
	if permit == "" {
		return true
	}
	for _, p := range permissions {
		if p == permit {
			// access the permit
			return true
		}
	}
	return false
}

// verifyDashboard check whether the user is valid from Dashboard.
func (p *Permit) verifyDashboard(ctx *bm.Context, sid string) (username string, err error) {
	params := url.Values{}
	params.Set("session_id", sid)
	params.Set("encrypt", "md5")
	params.Set("caller", p.dashboardCaller)
	var res struct {
		Code     int    `json:"code"`
		Message  string `json:"message"`
		UserName string `json:"username"`
	}
	if err = p.dsClient.Get(ctx, p.verifyURI, xmetadata.String(ctx, xmetadata.RemoteIP), params, &res); err != nil {
		log.Errorf("dashboard get verify Session url(%s) error(%v)", p.verifyURI+"?"+params.Encode(), err)
		return
	}
	if res.Code != ecode.OK.Code() {
		log.Errorf("dashboard get verify Session url(%s) error(%v)", p.verifyURI+"?"+params.Encode(), res.Code)
		err = ecode.New(res.Code, res.Message) // ecode.Int(res.Code)
		return
	}
	username = res.UserName
	return
}

// permissions get user's permisssions from manager-admin.
func (p *Permit) permissions(ctx *bm.Context, username string) (uid int64, perms []string, err error) {
	params := url.Values{}
	params.Set(_sessUnKey, username)
	var res struct {
		Code int         `json:"code"`
		Data permissions `json:"data"`
	}
	if err = p.maClient.Get(ctx, p.permissionURI, xmetadata.String(ctx, xmetadata.RemoteIP), params, &res); err != nil {
		log.Errorf("dashboard get permissions url(%s) error(%v)", p.permissionURI+"?"+params.Encode(), err)
		return
	}
	if res.Code != ecode.OK.Code() {
		log.Errorf("dashboard get permissions url(%s) error(%v)", p.permissionURI+"?"+params.Encode(), res.Code)
		err = ecode.New(res.Code, "permit error")
		return
	}
	perms = res.Data.Perms
	uid = res.Data.UID
	return
}

// WithPermitContext 校验当前用户并获取其权限相关数据，放入context
func (p *Permit) WithPermitContext() bm.HandlerFunc {
	return func(ctx *bm.Context) {
		sid, username, _, err := p.login2(ctx)
		if err != nil {
			ctx.JSON(nil, ecode.Unauthorized)
			ctx.Abort()
			return
		}
		ctx.Set(_sessUnKey, username)
		ctx.Set(_sessIDKey, sid)
		p.sm.setHTTPCookie(ctx, _defaultCookieName, sid)
		reply, err := p.Permissions(ctx, &api.PermissionReq{Username: username})
		if err != nil {
			if ecode.EqualError(ecode.NothingFound, err) {
				ctx.JSON(nil, ecode.AccessDenied)
				ctx.Abort()
			}
			return
		}
		ctx.Set(_sessUIDKey, reply.Uid)
		if len(reply.Perms) > 0 {
			ctx.Set(CtxPermissionsKey, reply.Perms)
		}
		if len(reply.GroupIds) > 0 {
			ctx.Set(CtxGroupIDsKey, reply.GroupIds)
		}
		if len(reply.RoleIds) > 0 {
			ctx.Set(CtxRoleIDsKey, reply.RoleIds)
		}
	}
}
